﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Timers;
using log4net;

namespace AccessBasedDesktop.Core
{
    /// <summary>
    /// Functional class
    /// </summary>
    public class Manager : IDisposable
    {
        /// <summary>
        ///     Log manager
        /// </summary>
        private static readonly ILog Log = LogManager.GetLogger(typeof (Manager));
        /// <summary>
        ///     Event log
        /// </summary>
        private readonly EventLog eventLog;
        /// <summary>
        ///     Desktop source directory state
        /// </summary>
        private readonly List<FileSystemData> fileDataList = new List<FileSystemData>();
        /// <summary>
        ///     Update timer
        /// </summary>
        private readonly Timer timer;
        private bool autoUpdateEnabled;
        private bool inUpdate;
        private bool isDisposed;
        /// <summary>
        ///     Last processed event index
        /// </summary>
        private int lastEventIndex;
        private Options options;
        /// <summary>
        ///     Timer elapse count
        /// </summary>
        private int timerCount;
        private FileSystemWatcher watcher;

        public Manager(Options options)
        {
            Options = options;

            timer = new Timer();
            timer.Interval = 1000;
            timer.Elapsed += timerOnElapsed;
            timer.Enabled = true;

            eventLog = new EventLog(@"Security");
            eventLog.EntryWritten += eventLogOnEntryWritten;
            eventLog.EnableRaisingEvents = true;

            lastEventIndex = eventLog.Entries.Count - 1;
            autoUpdateEnabled = true;

            loadDesktopFileList();
        }

        /// <summary>
        ///     Enable/Disable autoupdate
        /// </summary>
        public bool AutoUpdateEnabled
        {
            get { return autoUpdateEnabled; }
            set
            {
                autoUpdateEnabled = value;
                Log.InfoFormat(@"Auto update {0}", autoUpdateEnabled ? @"started" : @"stopped");
            }
        }

        /// <summary>
        ///     Current options
        /// </summary>
        public Options Options
        {
            get { return options; }
            set
            {
                options = value;

                if (watcher != null)
                {
                    watcher.Changed -= watcherOnChanged;
                    watcher.Dispose();
                    watcher = null;
                }

                if (options != null && options.WatchForChages && Directory.Exists(options.GetCurrentDesktopDir()))
                {
                    loadDesktopFileList();

                    UpdateAllDesktops();

                    watcher = new FileSystemWatcher(Options.GetCurrentDesktopDir());
                    watcher.Changed += watcherOnChanged;
                    watcher.Created += watcherOnCreated;
                    watcher.Deleted += watcherOnDeleted;
                    watcher.Renamed += watcherOnRenamed;

                    watcher.NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime
                                           | NotifyFilters.DirectoryName | NotifyFilters.FileName
                                           | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size;

                    watcher.EnableRaisingEvents = true;
                }
            }
        }

        /// <summary>
        /// Dispose resources
        /// </summary>
        public void Dispose()
        {
            if (!isDisposed)
            {
                isDisposed = true;
                timer.Dispose();
                eventLog.Dispose();
            }
        }

        /// <summary>
        ///     Save desktop source directory state
        /// </summary>
        /// <param name="list"></param>
        private void loadDesktopFileList(List<FileSystemData> list = null)
        {
            string sourceDir = Options.GetCurrentDesktopDir();
            if (Directory.Exists(sourceDir))
            {
                list = list ?? fileDataList;
                list.Clear();
                var allEntries = Directory.GetFileSystemEntries(sourceDir, @"*.*", SearchOption.AllDirectories);
                list.AddRange(allEntries.Select(x => new FileSystemData(File.Exists(x)
                    ? new FileInfo(x)
                    : new DirectoryInfo(x) as FileSystemInfo)));
            }
        }

        /// <summary>
        /// Update time event handler
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="elapsedEventArgs"></param>
        private void timerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
        {
            if (!AutoUpdateEnabled) return;

            timer.Enabled = false;
            timerCount++;
            if (Options != null && Options.ScanOnTimer)
            {
                if (timerCount >= Options.TimerInterval)
                {
                    doWatcherUpdate();
                    timerCount = 0;
                }
            }
            else
            {
                timerCount = 0;
            }
            timer.Enabled = true;
        }

        /// <summary>
        ///     Security log event handler
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void eventLogOnEntryWritten(object sender, EntryWrittenEventArgs e)
        {
            if (!AutoUpdateEnabled) return;

            if (Options != null && Options.UseJornal)
            {
                // for each new event
                int currentEventIndex = eventLog.Entries.Count - 1;
                while (currentEventIndex > lastEventIndex)
                {
                    var entry = eventLog.Entries[currentEventIndex];
                    if (Options.IsValidEventCode(entry.InstanceId))
                        UpdateAllDesktops();
                    currentEventIndex--;
                }
            }
            lastEventIndex = eventLog.Entries.Count - 1;
        }

        private void watcherOnRenamed(object sender, RenamedEventArgs renamedEventArgs)
        {
            doWatcherUpdate();
        }

        private void watcherOnDeleted(object sender, FileSystemEventArgs fileSystemEventArgs)
        {
            doWatcherUpdate();
        }

        private void watcherOnCreated(object sender, FileSystemEventArgs fileSystemEventArgs)
        {
            doWatcherUpdate();
        }

        private void deleteDesktopElement(string fullPath)
        {
            string oldElementName = fullPath.Replace(Options.GetCurrentDesktopDir() + @"\", "");
            scanAllUsers(Options, oldElementName, deleteUserDesktopElement);
        }

        private void deleteUserDesktopElement(Options theOptions, string oldName, string userName)
        {
            var userDesktopDir = getUserDesktopDir(userName);
            var oldFileName = Path.Combine(userDesktopDir, oldName);
            if (Directory.Exists(oldFileName))
            {
                Directory.Delete(oldFileName, true);
            }
            if (File.Exists(oldFileName))
            {
                File.Delete(oldFileName);
            }
        }

        private void copyDesktopElement(string fullPath)
        {
            var elementName = fullPath.Replace(Options.GetCurrentDesktopDir() + @"\", "");
            scanAllUsers(Options, elementName, copyUserDesktopElement);
        }

        private void copyUserDesktopElement(Options theOptions, string elementName, string userName)
        {
            var sourceFileName = Path.Combine(theOptions.GetCurrentDesktopDir(), elementName);
            var userDesktopDir = Path.Combine(getUserDesktopDir(userName), Path.GetDirectoryName(elementName));
            copyFileSystemElement(sourceFileName, userDesktopDir, userName);
        }

        private static string getUserDesktopDir(string userName)
        {
            var usersDir = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
            usersDir = Path.GetDirectoryName(usersDir);
            usersDir = Path.GetDirectoryName(usersDir);
            return Path.Combine(usersDir, Path.GetFileName(userName), @"Desktop");
        }

        private void watcherOnChanged(object sender, FileSystemEventArgs e)
        {
            if (!AutoUpdateEnabled) return;

            doWatcherUpdate();
        }

        private void doWatcherUpdate()
        {
            var newDesktopFiles = new List<FileSystemData>();
            loadDesktopFileList(newDesktopFiles);

            var deleted = fileDataList.Except(newDesktopFiles).ToList();
            var modified = fileDataList.Where(x => x.IsModified()).ToList();
            var created = newDesktopFiles.Except(fileDataList).ToList();

            deleted.ForEach(x => deleteDesktopElement(x.FullName));
            created.ForEach(x => copyDesktopElement(x.FullName));
            modified.ForEach(x => copyDesktopElement(x.FullName));

            loadDesktopFileList();
        }

        public void UpdateAllDesktops(Options theOptions = null)
        {
            if (inUpdate)
                return;

            inUpdate = true;

            Log.Info(@"UpdateAllDesktops enter");

            if (theOptions == null)
                theOptions = Options;

            if (theOptions == null)
            {
                Log.Warn(@"Options is null - skip UpdateAllDesktops");
            }
            else
            {
                var sourceDesktopDir = theOptions.GetCurrentDesktopDir();
                Log.Debug(string.Format(@"sourceDesktopDir={0}", sourceDesktopDir));

                scanAllUsers(theOptions, sourceDesktopDir, updateUserDesktop);
            }

            Log.Info(@"UpdateAllDesktops leave");
            inUpdate = false;
        }

        private void scanAllUsers<T>(Options theOptions, T scanParams, Action<Options, T, string> scanAction)
        {
            var ctx = new PrincipalContext(theOptions.ContextType, theOptions.MachineName);

            IEnumerable<Principal> userPrincipals;

            if (theOptions.UseDomain)
            {
                var grp = GroupPrincipal.FindByIdentity(ctx, IdentityType.SamAccountName, @"Domain Users");
                userPrincipals = grp.GetMembers();
            }
            else
            {
                var user = new UserPrincipal(ctx);
                user.Name = "*";
                var ps = new PrincipalSearcher();
                ps.QueryFilter = user;
                userPrincipals = ps.FindAll();
            }

            Log.Debug(string.Format(@"Found {0} users", userPrincipals.Count()));

            foreach (var principal in userPrincipals)
            {
                using (var userPrincipal = (UserPrincipal) principal)
                {
                    if (!string.IsNullOrEmpty(userPrincipal.Name))
                    {
                        var isExcludedUser = theOptions.ExcludedUsersList.Contains(userPrincipal.Name);

                        if (!isExcludedUser)
                        {
                            var isWellKnown =
                                Enum.GetValues(typeof (WellKnownSidType))
                                    .Cast<WellKnownSidType>()
                                    .Aggregate(false, (y, z) => y || userPrincipal.Sid.IsWellKnown(z));

                            if (!isWellKnown)
                            {
                                var isInExcludedRole = theOptions.ExcludedRolesList.Aggregate(false,
                                    (x, y) => x || userPrincipal.IsMemberOf(new GroupPrincipal(ctx, y)));

                                if (!isInExcludedRole)
                                    scanAction(theOptions, scanParams,
                                        theOptions.MachineName + @"\" + userPrincipal.Name);
                                else
                                    Log.Debug(string.Format(@"User {0} is in role marked excluded - skip it",
                                        userPrincipal.Name));
                            }
                            else
                                Log.Debug(string.Format(@"User {0} is well known - skip it", userPrincipal.Name));
                        }
                        else
                            Log.Debug(string.Format(@"User {0} marked excluded - skip it", userPrincipal.Name));
                    }
                }
            }
        }

        private void updateUserDesktop(Options theOptions, string sourceDesktopDir, string userName)
        {
            Log.Debug(string.Format(@"UpdateUserDesktop enter with sourceDesktopDir={0} and userName={1}",
                sourceDesktopDir, userName));

            var destDesktopDir = getUserDesktopDir(userName);

            if (Directory.Exists(sourceDesktopDir))
            {
                if (Directory.Exists(destDesktopDir))
                {
                    foreach (string fileSystemEntry in Directory.GetFileSystemEntries(sourceDesktopDir))
                    {
                        copyFileSystemElement(fileSystemEntry, destDesktopDir, userName);
                    }
                }
                else
                    Log.Debug(string.Format(@"User desktop dir {0} is not found", destDesktopDir));
            }
            else
                Log.Debug(string.Format(@"Source desktop dir {0} is not found", sourceDesktopDir));

            Log.Debug(@"UpdateUserDesktop leave");
        }

        private bool copyFileSystemElement(string sourceElementName, string destDirectory, string userName)
        {
            Log.Debug(
                string.Format(@"CopyFileSystemElement enter with sourceDesktopDir={0} destDirectory={1} userName={2}",
                    sourceElementName, destDirectory, userName));

            var isDir = Directory.Exists(sourceElementName);
            var isFile = File.Exists(sourceElementName);

            Func<string, bool> existsMethod = null;
            Action<string> deleteMethod = null;
            Action<string, string, string> copyMethod = null;

            if (isDir)
            {
                Log.Debug(@"Source element is directory");
                existsMethod = Directory.Exists;
                deleteMethod = Directory.Delete;
                copyMethod = copyDirectory;
            }

            if (isFile)
            {
                Log.Debug(@"Source element is file");
                existsMethod = File.Exists;
                deleteMethod = File.Delete;
                copyMethod = copyFile;
            }

            var result = (isDir || isFile) && Directory.Exists(destDirectory);
            if (result)
            {
                FileSystemRights rights = 0;
                result = false;
                try
                {
                    rights = GetFileEffectiveRights(sourceElementName, userName);
                }
                catch (Exception exception)
                {
                    Log.Debug(string.Format(@"Exception {0} in GetFileEffectiveRights", exception.Message));
                }

                var fileName = Path.GetFileName(sourceElementName);
                var destElementName = Path.Combine(destDirectory, fileName);

                Log.Debug(string.Format(@"Dest element name is {0}", destElementName));

                if ((rights & FileSystemRights.Read) == FileSystemRights.Read)
                {
                    Log.Debug(string.Format(@"User {0} can read source element ... copy it to user desktop", userName));

                    copyMethod(sourceElementName, destElementName, userName);
                    result = true;
                }
                else
                {
                    Log.Debug(string.Format(@"User {0} can not read source element", userName));
                    if (existsMethod(destElementName))
                    {
                        Log.Debug(string.Format(@"Destination element {0} already exists ... try to delete it",
                            destElementName));
                        try
                        {
                            deleteMethod(destElementName);
                            Log.Debug(string.Format(@"Destination element {0} deleted", destElementName));
                        }
                        catch (Exception exception)
                        {
                            Log.Debug(string.Format(@"Exception {0} in GetFileEffectiveRights while deleting",
                                exception.Message));
                        }
                    }
                }
            }

            Log.Debug(@"CopyFileSystemElement leave");

            return result;
        }

        private void copyFile(string source, string dest, string userName)
        {
            File.Copy(source, dest, true);

            var fsc = File.GetAccessControl(source);
            var sdb = fsc.GetSecurityDescriptorBinaryForm();
            var fileSec = new FileSecurity();
            fileSec.SetSecurityDescriptorBinaryForm(sdb);

            File.GetAccessControl(dest).SetAccessRuleProtection(true, false);
            File.SetAccessControl(dest, fileSec);
        }

        private void copyDirectory(string source, string dest, string userName)
        {
            if (Directory.Exists(source))
            {
                if (!Directory.Exists(dest))
                {
                    if ((GetFileEffectiveRights(source, userName) & FileSystemRights.ReadData) ==
                        FileSystemRights.ReadData)
                    {
                        Directory.CreateDirectory(dest);
                    }
                }

                var fsc = Directory.GetAccessControl(source);
                var sdb = fsc.GetSecurityDescriptorBinaryForm();
                var disec = new DirectorySecurity();
                disec.SetSecurityDescriptorBinaryForm(sdb);
                Directory.SetAccessControl(dest, disec);
                Directory.GetAccessControl(dest).SetAccessRuleProtection(true, false);

                foreach (string fileSystemEntry in Directory.GetFileSystemEntries(source))
                {
                    copyFileSystemElement(fileSystemEntry, dest, userName);
                }
            }
        }

        public static FileSystemRights GetFileEffectiveRights(string fileName, string userName)
        {
            IntPtr pDacl, pZero = IntPtr.Zero;
            int mask = 0;
            uint errorReturn = GetNamedSecurityInfo(fileName, SE_OBJECT_TYPE.SE_FILE_OBJECT, SECURITY_INFORMATION.Dacl
                , out pZero, out pZero, out pDacl, out pZero, out pZero);
            if (errorReturn != 0)
            {
                throw new Exception("Win error : " + errorReturn);
            }
            var pTrustee = new TRUSTEE();
            pTrustee.pMultipleTrustee = IntPtr.Zero;
            pTrustee.MultipleTrusteeOperation = MULTIPLE_TRUSTEE_OPERATION.NO_MULTIPLE_TRUSTEE;
            pTrustee.Name = userName;
            pTrustee.TrusteeForm = TRUSTEE_FORM.TRUSTEE_IS_NAME;
            pTrustee.TrusteeType = TRUSTEE_TYPE.TRUSTEE_IS_USER;
            errorReturn = GetEffectiveRightsFromAcl(pDacl, ref pTrustee, ref mask);
            if (errorReturn != 0)
            {
                throw new Exception("Win error : " + errorReturn);
            }
            return (FileSystemRights) mask;
        }

        [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
        private static extern uint GetNamedSecurityInfo(
            string pObjectName,
            SE_OBJECT_TYPE ObjectType,
            SECURITY_INFORMATION SecurityInfo,
            out IntPtr pSidOwner,
            out IntPtr pSidGroup,
            out IntPtr pDacl,
            out IntPtr pSacl,
            out IntPtr pSecurityDescriptor);

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern uint GetEffectiveRightsFromAcl(IntPtr pDacl, ref TRUSTEE pTrustee, ref int pAccessRights);

        /// <summary>
        ///     FileSystemInfo state
        /// </summary>
        private class FileSystemData
        {
            public FileSystemData(FileSystemInfo info)
            {
                FullName = info.FullName;
                LastWriteTime = info.LastWriteTime;
                AccessString = info.GetAccessString();
            }

            /// <summary>
            ///     ACL string
            /// </summary>
            public string AccessString { get; set; }

            /// <summary>
            ///     Item full name
            /// </summary>
            public string FullName { get; set; }

            /// <summary>
            ///     Last change/write timestamp
            /// </summary>
            public DateTime LastWriteTime { get; set; }

            /// <summary>
            ///     Whether item is modified (ACL changed)
            /// </summary>
            /// <returns></returns>
            public bool IsModified()
            {
                FileSystemInfo fsi = File.Exists(FullName)
                    ? new FileInfo(FullName)
                    : (FileSystemInfo) new DirectoryInfo(FullName);
                return fsi.Exists && (fsi.LastWriteTime != LastWriteTime || AccessString != fsi.GetAccessString());
            }

            #region Equality implementation

            public override bool Equals(object obj)
            {
                return obj is FileSystemData ? (obj as FileSystemData).FullName.Equals(FullName) : base.Equals(obj);
            }

            public override int GetHashCode()
            {
                return string.IsNullOrEmpty(FullName) ? 0 : FullName.GetHashCode();
            }

            #endregion
        }
    }

    public enum MULTIPLE_TRUSTEE_OPERATION
    {
        NO_MULTIPLE_TRUSTEE,
        TRUSTEE_IS_IMPERSONATE
    }

    public enum TRUSTEE_FORM
    {
        TRUSTEE_IS_SID,
        TRUSTEE_IS_NAME,
        TRUSTEE_BAD_FORM,
        TRUSTEE_IS_OBJECTS_AND_SID,
        TRUSTEE_IS_OBJECTS_AND_NAME
    }

    public enum TRUSTEE_TYPE
    {
        TRUSTEE_IS_UNKNOWN,
        TRUSTEE_IS_USER,
        TRUSTEE_IS_GROUP,
        TRUSTEE_IS_DOMAIN,
        TRUSTEE_IS_ALIAS,
        TRUSTEE_IS_WELL_KNOWN_GROUP,
        TRUSTEE_IS_DELETED,
        TRUSTEE_IS_INVALID,
        TRUSTEE_IS_COMPUTER
    }

    //Platform independent (32 & 64 bit) - use Pack = 0 for both platforms. IntPtr works as well.
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 0)]
    public struct TRUSTEE : IDisposable
    {
        public IntPtr pMultipleTrustee;
        public MULTIPLE_TRUSTEE_OPERATION MultipleTrusteeOperation;
        public TRUSTEE_FORM TrusteeForm;
        public TRUSTEE_TYPE TrusteeType;
        public IntPtr ptstrName;

        void IDisposable.Dispose()
        {
            if (ptstrName != IntPtr.Zero) Marshal.Release(ptstrName);
        }

        public string Name
        {
            get { return Marshal.PtrToStringUni(ptstrName); }
            set { ptstrName = Marshal.StringToHGlobalUni(value); }
        }
    }

    [Flags]
    internal enum SECURITY_INFORMATION : uint
    {
        Owner = 0x00000001,
        Group = 0x00000002,
        Dacl = 0x00000004,
        Sacl = 0x00000008,
        ProtectedDacl = 0x80000000,
        ProtectedSacl = 0x40000000,
        UnprotectedDacl = 0x20000000,
        UnprotectedSacl = 0x10000000
    }


    public enum SE_OBJECT_TYPE
    {
        SE_UNKNOWN_OBJECT_TYPE = 0,
        SE_FILE_OBJECT,
        SE_SERVICE,
        SE_PRINTER,
        SE_REGISTRY_KEY,
        SE_LMSHARE,
        SE_KERNEL_OBJECT,
        SE_WINDOW_OBJECT,
        SE_DS_OBJECT,
        SE_DS_OBJECT_ALL,
        SE_PROVIDER_DEFINED_OBJECT,
        SE_WMIGUID_OBJECT,
        SE_REGISTRY_WOW64_32KEY
    }
}